package org.jix.gbook.note.biz.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.jix.framework.biz.context.holder.LoginUserContextHolder;
import org.jix.framework.common.exception.BizException;
import org.jix.framework.common.response.Response;
import org.jix.framework.common.util.JsonUtils;
import org.jix.gbook.note.biz.constant.MQConstants;
import org.jix.gbook.note.biz.constant.RedisKeyConstants;
import org.jix.gbook.note.biz.domain.dataObject.NoteDO;
import org.jix.gbook.note.biz.domain.mapper.NoteDOMapper;
import org.jix.gbook.note.biz.domain.mapper.TopicDOMapper;
import org.jix.gbook.note.biz.enums.NoteStatusEnum;
import org.jix.gbook.note.biz.enums.NoteTypeEnum;
import org.jix.gbook.note.biz.enums.NoteVisibleEnum;
import org.jix.gbook.note.biz.enums.ResponseCodeEnum;
import org.jix.gbook.note.biz.module.vo.*;
import org.jix.gbook.note.biz.rpc.DistributedIdGeneratorRpcService;
import org.jix.gbook.note.biz.rpc.KeyValueRpcService;
import org.jix.gbook.note.biz.rpc.UserRpcService;
import org.jix.gbook.note.biz.service.NoteService;
import org.jix.gbook.user.dto.resp.FindUserByIdRspDTO;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author stark
 * @date 2025/8/30 下午3:50
 * @description
 **/
@Service
@Slf4j
public class NoteServiceImpl implements NoteService {

    @Resource
    private NoteDOMapper noteDOMapper;
    @Resource
    private TopicDOMapper topicDOMapper;
    @Resource
    private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
    @Resource
    private KeyValueRpcService keyValueRpcService;
    @Resource
    private UserRpcService userRpcService;
    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Resource
    private RedisTemplate<String,String> redisTemplate;
    @Resource
    private RocketMQTemplate rocketMQTemplate;

    @Override
    public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
        //笔记类型
        Integer type = publishNoteReqVO.getType();

        //获取对应的枚举
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);

        //若非图文或者视频则抛出异常
        if (Objects.isNull(noteTypeEnum)){
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }
        String imgUrls = null;
        //笔记内容默认为空
        Boolean isContentEmpty = true;
        String videoUri = null;
        switch (noteTypeEnum) {
            case IMAGE_TEXT: //图文笔记
                List<String> imgUriList = publishNoteReqVO.getImgUris();
                //校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList),"笔记图片不能为空");
                //校验数量
                Preconditions.checkArgument(imgUriList.size() <= 8,"笔记图片最多八张");
                //拼接图片链接
                imgUrls = StringUtils.join(imgUriList,",");
                break;
            case VIDEO: //视频笔记
                videoUri = publishNoteReqVO.getVideoUri();
                //校验视频链接是否为空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri),"笔记视频不能为空");
                break;
            default:
                break;
        }

        //生成笔记ID-RPC调用分布式ID生成服务
        String snowflakeId = distributedIdGeneratorRpcService.getSnowflakeId();
        //笔记内容UUID
        String contentUuid = null;

        //笔记内容
        String content = publishNoteReqVO.getContent();

        //判断用户是否填写了笔记内容
        if(StringUtils.isNotBlank(content)){
            isContentEmpty = false;
            //生成笔记内容UUID
            contentUuid = UUID.randomUUID().toString();
            //RPC：调用KV键值服务存储文本
            boolean isSavedSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);

            //存储失败
            if (!isSavedSuccess){
                throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
            }
        }

        //话题
        Long topicId = publishNoteReqVO.getTopicId();
        String topicName = null;

        //判断话题是否为空
        if (Objects.nonNull(topicId)){
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
        }

        //发布者ID
        Long creatorId = LoginUserContextHolder.getUserID();

        //构建笔记对象
        NoteDO noteDO = NoteDO.builder()
                .id(Long.valueOf(snowflakeId))
                .isContentEmpty(isContentEmpty)
                .creatorId(creatorId)
                .imgUris(imgUrls)
                .title(publishNoteReqVO.getTitle())
                .topicId(publishNoteReqVO.getTopicId())
                .topicName(topicName)
                .type(type)
                .visible(NoteVisibleEnum.PUBLIC.getCode())
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .status(NoteStatusEnum.NORMAL.getCode())
                .isTop(Boolean.FALSE)
                .videoUri(videoUri)
                .contentUuid(contentUuid)
                .build();
        try {
            //笔记入库
            noteDOMapper.insertSelective(noteDO);
        } catch (Exception e) {
            log.error("==> 笔记存入失败",e);
            //RPC 笔记保存失败则删除笔记内容
            if (StringUtils.isNotBlank(contentUuid)){
                keyValueRpcService.deleteNoteContent(contentUuid);
            }
        }
        return Response.success();
    }

    /**
     * 查询笔记详情
     * @param findNoteDetailReqVO
     * @return
     */
    @Override
    public Response<FindNoteDetailRspVO> findNoteDetail(FindNoteDetailReqVO findNoteDetailReqVO) {
        //笔记ID
        Long noteId = findNoteDetailReqVO.getId();
        //当前登录用户
        Long userID = LoginUserContextHolder.getUserID();

        // 先从本地缓存中查询
        String findNoteDetailRspVOStrLocalCache = LOCAL_CACHE.getIfPresent(noteId);
        if (StringUtils.isNotBlank(findNoteDetailRspVOStrLocalCache)) {
            FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(findNoteDetailRspVOStrLocalCache, FindNoteDetailRspVO.class);
            log.info("==> 命中了本地缓存；{}", findNoteDetailRspVOStrLocalCache);
            // 可见性校验
            checkNoteVisibleFromVO(userID, findNoteDetailRspVO);
            return Response.success(findNoteDetailRspVO);
        }

        // 从 Redis 缓存中获取
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteDetailJson = redisTemplate.opsForValue().get(noteDetailRedisKey);

        // 若缓存中有该笔记的数据，则直接返回
        if (StringUtils.isNotBlank(noteDetailJson)) {
            FindNoteDetailRspVO findNoteDetailRspVO = JsonUtils.parseObject(noteDetailJson, FindNoteDetailRspVO.class);
            // 异步线程中将用户信息存入本地缓存
            threadPoolTaskExecutor.submit(() -> {
                // 写入本地缓存
                LOCAL_CACHE.put(noteId,
                        Objects.isNull(findNoteDetailRspVO) ? "null" : JsonUtils.toJsonString(findNoteDetailRspVO));
            });
            // 可见性校验
            checkNoteVisibleFromVO(userID, findNoteDetailRspVO);

            return Response.success(findNoteDetailRspVO);
        }

        // 若 Redis 缓存中获取不到，则走数据库查询
        // 查询笔记
        NoteDO noteDO = noteDOMapper.selectByPrimaryKey(noteId);

        // 若该笔记不存在，则抛出业务异常
        if (Objects.isNull(noteDO)) {
            threadPoolTaskExecutor.execute(() -> {
                // 防止缓存穿透，将空数据存入 Redis 缓存 (过期时间不宜设置过长)
                // 保底1分钟 + 随机秒数
                long expireSeconds = 60 + RandomUtil.randomInt(60);
                redisTemplate.opsForValue().set(noteDetailRedisKey, "null", expireSeconds, TimeUnit.SECONDS);
            });
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        Long creatorId = noteDO.getCreatorId();

        //是否仅自己可见
        Integer visible = noteDO.getVisible();
        checkNoteVisible(visible,userID, creatorId);

        //RPC：调用用户服务
        CompletableFuture<FindUserByIdRspDTO> userResultFuture = CompletableFuture.supplyAsync(
                () -> userRpcService.findById(creatorId), threadPoolTaskExecutor);

        //RPC：调用k-v存储服务获取内容
        CompletableFuture<String> contentResultFuture = CompletableFuture.completedFuture(null);
        if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)){
            contentResultFuture = CompletableFuture.supplyAsync(
                    () -> keyValueRpcService.findNoteContent(noteDO.getContentUuid()),threadPoolTaskExecutor);
        }
        CompletableFuture<String> contentResultFinalFuture = contentResultFuture;
        CompletableFuture<FindNoteDetailRspVO> resultFuture = CompletableFuture.allOf(userResultFuture, contentResultFuture).thenApply(s -> {
            //获取返回结果
            FindUserByIdRspDTO findUserByIdRspDTO = userResultFuture.join();
            String content = contentResultFinalFuture.join();

            //笔记类型
            Integer noteType = noteDO.getType();
            //图文链接
            String imgUrisStr = noteDO.getImgUris();
            //链接集合
            List<String> imgUris = null;
            if (Objects.equals(noteType, NoteTypeEnum.IMAGE_TEXT.getCode())
                    && StringUtils.isNotBlank(imgUrisStr)
            ) {
                imgUris = List.of(imgUrisStr.split(","));
            }
            //构建反参
            return FindNoteDetailRspVO.builder()
                    .id(noteDO.getId())
                    .type(noteType)
                    .imgUris(imgUris)
                    .title(noteDO.getTitle())
                    .content(content)
                    .topicId(noteDO.getTopicId())
                    .topicName(noteDO.getTopicName())
                    .creatorId(creatorId)
                    .creatorName(findUserByIdRspDTO.getNickName())
                    .avatar(findUserByIdRspDTO.getAvatar())
                    .videoUri(noteDO.getVideoUri())
                    .visible(visible)
                    .updateTime(noteDO.getUpdateTime())
                    .build();
        });

        //获取拼装后的findNoteDetailRspVO
        FindNoteDetailRspVO findNoteDetailRspVO = null;
        try {
            findNoteDetailRspVO = resultFuture.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }

        // 异步线程中将笔记详情存入 Redis
        FindNoteDetailRspVO finalFindNoteDetailRspVO = findNoteDetailRspVO;
        threadPoolTaskExecutor.submit(() -> {
            String noteDetailJson1 = JsonUtils.toJsonString(finalFindNoteDetailRspVO);
            // 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
            long expireSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);
            redisTemplate.opsForValue().set(noteDetailRedisKey, noteDetailJson1, expireSeconds, TimeUnit.SECONDS);
        });

        return Response.success(findNoteDetailRspVO);
    }

    /**
     * 笔记更新
     * @param updateNoteReqVO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
        //获取笔记Id
        Long noteId = updateNoteReqVO.getId();
        //笔记类型
        Integer noteType = updateNoteReqVO.getType();

        //获取对应枚举
        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(noteType);
        //未知类型
        if (Objects.isNull(noteTypeEnum)){
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }

        String imgUris = null;
        String videoUri = null;

        switch (noteTypeEnum){
            case IMAGE_TEXT: //图文
                List<String> imgUriList = updateNoteReqVO.getImgUris();
                //校验图片是否为空
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList),"笔记图片不能为空");
                //校验图片数量
                Preconditions.checkArgument(imgUriList.size() <= 8,"笔记图片最多八张");

                imgUris = StringUtils.join(imgUriList,",");
                break;
            case VIDEO: //视频
                videoUri = updateNoteReqVO.getVideoUri();
                //校验视频是否为空
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri),"笔记视频不能为空");
                break;
            default:
                break;
        }

        //话题
        Long topicId = updateNoteReqVO.getTopicId();
        String topicName = null;
        //判空
        if (Objects.nonNull(topicId)){
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
            //判空
            if (StringUtils.isBlank(topicName)){
                throw new BizException(ResponseCodeEnum.TOPIC_NOT_FOUND);
            }
        }

        // 删除 Redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        //更新笔记
        String content = updateNoteReqVO.getContent();
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isContentEmpty(StringUtils.isBlank(content))
                .type(noteType)
                .imgUris(imgUris)
                .videoUri(videoUri)
                .topicId(topicId)
                .topicName(topicName)
                .title(updateNoteReqVO.getTitle())
                .updateTime(LocalDateTime.now())
                .build();

        noteDOMapper.updateByPrimaryKey(noteDO);

        // 一致性保证：延迟双删策略
        // 异步发送延时消息
        Message<String> message = MessageBuilder.withPayload(String.valueOf(noteId))
                .build();

        rocketMQTemplate.asyncSend(MQConstants.TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE, message,
                new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        log.info("## 延时删除 Redis 笔记缓存消息发送成功...");
                    }

                    @Override
                    public void onException(Throwable e) {
                        log.error("## 延时删除 Redis 笔记缓存消息发送失败...", e);
                    }
                },
                3000, // 超时时间(毫秒)
                1 // 延迟级别，1 表示延时 1s
        );


        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        //笔记内容更新
        //获取UUID
        NoteDO noteDO1 = noteDOMapper.selectByPrimaryKey(noteId);
        String contentUuid = noteDO1.getContentUuid();

        //判断笔记是否更新成功
        Boolean isUpdateContentSuccess = false;
        if (StringUtils.isBlank(content)){
            //笔记内容为空，则删除k-v存储
            isUpdateContentSuccess = keyValueRpcService.deleteNoteContent(contentUuid);
        } else {
            //无内容笔记更新内容，应重新生成UUID
            contentUuid = StringUtils.isBlank(contentUuid) ? UUID.randomUUID().toString() : contentUuid;
            //调用k-v更新文本
            isUpdateContentSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
        }

        //更新失败
        if (!isUpdateContentSuccess){
            throw new BizException(ResponseCodeEnum.NOTE_UPDATE_FAIL);
        }
        return Response.success();
    }

    /**
     * 删除本地笔记缓存
     * @param noteId
     */
    @Override
    public void deleteNoteLocalCache(Long noteId) {
        LOCAL_CACHE.invalidate(noteId);
    }

    /**
     * 删除笔记
     * @param deleteNoteReqVO
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
        // 笔记 ID
        Long noteId = deleteNoteReqVO.getId();

        // 逻辑删除
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .status(NoteStatusEnum.DELETED.getCode())
                .updateTime(LocalDateTime.now())
                .build();

        int count = noteDOMapper.updateByPrimaryKeySelective(noteDO);

        // 若影响的行数为 0，则表示该笔记不存在
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        // 删除缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }

    /**
     * 笔记仅对自己可见
     * @param updateNoteVisibleOnlyMeReqVO
     * @return
     */
    @Override
    public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
        // 笔记 ID
        Long noteId = updateNoteVisibleOnlyMeReqVO.getId();

        // 构建更新 DO 实体类
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .visible(NoteVisibleEnum.PRIVATE.getCode()) // 可见性设置为仅对自己可见
                .updateTime(LocalDateTime.now())
                .build();

        // 执行更新 SQL
        int count = noteDOMapper.updateVisibleOnlyMe(noteDO);

        // 若影响的行数为 0，则表示该笔记无法修改为仅自己可见
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_VISIBLE_ONLY_ME);
        }

        // 删除 Redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }


    /**
     * 笔记置顶 / 取消置顶
     * @param topNoteReqVO
     * @return
     */
    @Override
    public Response<?> topNote(TopNoteReqVO topNoteReqVO) {
        // 笔记 ID
        Long noteId = topNoteReqVO.getId();
        // 是否置顶
        Boolean isTop = topNoteReqVO.getIsTop();

        // 当前登录用户 ID
        Long currUserId = LoginUserContextHolder.getUserID();

        // 构建置顶/取消置顶 DO 实体类
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isTop(isTop)
                .updateTime(LocalDateTime.now())
                .creatorId(currUserId) // 只有笔记所有者，才能置顶/取消置顶笔记
                .build();

        int count = noteDOMapper.updateIsTop(noteDO);

        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        // 删除 Redis 缓存
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        // 同步发送广播模式 MQ，将所有实例中的本地缓存都删除掉
        rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }

    /**
     * 校验笔记可见性-VO实体类
     * @param userID
     * @param findNoteDetailRspVO
     */
    private void checkNoteVisibleFromVO(Long userID, FindNoteDetailRspVO findNoteDetailRspVO) {
        if (Objects.nonNull(findNoteDetailRspVO)) {
            Integer visible = findNoteDetailRspVO.getVisible();
            checkNoteVisible(visible, userID, findNoteDetailRspVO.getCreatorId());
        }
    }

    /**
     * 校验笔记可见性
     * @param visible
     * @param currUserId
     * @param creatorId
     */
    private void checkNoteVisible(Integer visible, Long currUserId, Long creatorId) {
        if (Objects.equals(visible,NoteVisibleEnum.PRIVATE.getCode())
                && !Objects.equals(currUserId,creatorId)
        ){
            //仅自己可见
            log.info("您无权查看这篇笔记");
            throw new BizException(ResponseCodeEnum.NOTE_PRIVATE);
        }
    }

    /**
     * 笔记详情本地缓存
     */
    private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000) // 设置初始容量为 10000 个条目
            .maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
            .expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
            .build();
}
